package model

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"unicode"

	"github.com/invopop/jsonschema"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/bsontype"
)

type (
	Ram struct {
		Unit  MemoryUnit
		Value int64
	}

	MemoryUnit string
)

var availableUnits = []string{"g", "gb", "k", "kb", "b", "m", "mb"}
var unitToValueMultiplier = map[string]int64{
	"g":  1000000000,
	"gb": 1000000000,
	"m":  1000000,
	"mb": 1000000,
	"k":  1000,
	"kb": 1000,
	"b":  1,
}

func (Ram) JSONSchema() *jsonschema.Schema {
	return &jsonschema.Schema{
		Type: "string",
	}
}

func (d *Ram) UnmarshalJSON(data []byte) error {
	var temp string
	if err := json.Unmarshal(data, &temp); err != nil {
		return fmt.Errorf("unmarshalling ram failed: %w", err)
	}
	if temp == "0" || temp == "" {
		*d = Ram{}
		return nil // todo: decide what to do w/ 0 value
	}

	var memory64 int64
	var unit string
	var isAvailableUnit = false
	var isMemoryUnitSpecified = false
	for i, c := range temp {
		if !unicode.IsDigit(c) {
			memory64, _ = strconv.ParseInt(temp[:i], 10, 64)
			unit = temp[i:]
			isMemoryUnitSpecified = true
			break
		}

	}
	if !isMemoryUnitSpecified {
		return fmt.Errorf("no memory unit specified")
	}
	unit = strings.ToLower(unit)
	for _, c := range availableUnits {
		if unit == c {
			isAvailableUnit = true
			break
		}
	}
	if isAvailableUnit {
		memoryUnit := MemoryUnit(unit)
		*d = Ram{
			Unit:  memoryUnit,
			Value: memory64,
		}
		return nil
	} else {
		return fmt.Errorf("unavailable memory unit: %s", unit)
	}
}

func (d *Ram) MarshalJSON() ([]byte, error) {
	temp := fmt.Sprint(d.Value) + string(d.Unit)
	return json.Marshal(temp)
}

func (d *Ram) UnmarshalBSONValue(t bsontype.Type, data []byte) error {
	raw := bson.RawValue{Type: t, Value: data}
	switch t {
	case bsontype.String:
		temp := raw.String()
		for i, c := range temp {
			if c == '"' && i == 0 {
				temp = temp[1:]
			}
			if c == '"' && i == len(temp) {
				temp = temp[:i-1]
			}
		}
		if temp == "0" {
			*d = Ram{}
			return nil // todo: decide what to do w/ 0 value
		}

		var memory64 int64
		var unit string
		var isAvailableUnit = false
		var isMemoryUnitSpecified = false
		for i, c := range temp {
			if !unicode.IsDigit(c) {
				memory64, _ = strconv.ParseInt(temp[:i], 10, 64)
				unit = temp[i:]
				isMemoryUnitSpecified = true
				break
			}

		}
		if !isMemoryUnitSpecified {
			return fmt.Errorf("no memory unit specified")
		}
		for _, c := range availableUnits {
			if unit == c {
				isAvailableUnit = true
				break
			}
		}
		if isAvailableUnit {
			memoryUnit := MemoryUnit(unit)
			*d = Ram{
				Unit:  memoryUnit,
				Value: memory64,
			}
			return nil
		} else {
			return fmt.Errorf("unavailable memory unit: %s", unit)
		}
	default:
		return fmt.Errorf("cannot unmarshall ram with type '%s'", t)
	}
}

func (d *Ram) MarshalBSONValue() (bsontype.Type, []byte, error) {
	temp := fmt.Sprint(d.Value) + string(d.Unit)
	return bson.MarshalValue(temp)
}

func (d *Ram) ToKubernetesRam() int64 {
	multiplier := unitToValueMultiplier[string(d.Unit)]

	result := multiplier * d.Value
	return result
}
